package net.avcompris.binding.impl;

import static com.avcompris.util.ExceptionUtils.nonNullArgument;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static net.avcompris.binding.impl.TypeUtils.findEnumConstant;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.substringAfter;
import static org.apache.commons.lang3.StringUtils.substringAfterLast;
import static org.apache.commons.lang3.StringUtils.substringBefore;
import static org.apache.commons.lang3.StringUtils.substringBeforeLast;
import static org.apache.commons.lang3.StringUtils.substringBetween;

import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import net.avcompris.binding.BindConfiguration;
import net.avcompris.binding.BindFunctions;
import net.avcompris.binding.Binder;
import net.avcompris.binding.BindingException;
import net.avcompris.binding.annotation.Functions;
import net.avcompris.binding.annotation.Namespaces;
import net.avcompris.binding.annotation.XPath;
import net.avcompris.binding.annotation.XPathFunctionNames;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ObjectUtils.Null;
import org.apache.commons.lang3.mutable.MutableBoolean;
import org.apache.commons.lang3.mutable.MutableInt;
import org.joda.time.DateTime;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.avcompris.common.annotation.Nullable;
import com.avcompris.lang.NotImplementedException;
import com.avcompris.util.XPathUtils;
import com.google.common.collect.Iterables;

/**
 * base class for {@link InvocationHandler} implementations returned by binders.
 * 
 * @author David Andrianavalontsalama
 */
public abstract class AbstractBinderInvocationHandler<U> extends
		InstanceUpdater<U> implements InvocationHandler {

	protected <T> AbstractBinderInvocationHandler(final Binder<U> binder,
			final ClassLoader classLoader, final Class<T> clazz,
			final U rootNode, final Class<U> nodeClass,
			// final String rootXPath,
			final BindConfiguration configuration) {

		this.binder = nonNullArgument(binder, "binder");
		this.classLoader = nonNullArgument(classLoader, "classLoader");
		this.configuration = nonNullArgument(configuration, "configuration");
		this.rootXPath = // nonNullArgument(rootXPath, "rootXPath");
		configuration.getXPathExpression();
		this.rootNode = nonNullArgument(rootNode, "rootNode");
		this.nodeClass = nonNullArgument(nodeClass, "nodeClass");
		this.clazz = nonNullArgument(clazz, "clazz");

		namespaces = calcNamespaceMap(clazz);
	}

	private final Binder<U> binder;
	private final Class<?> clazz;
	private final String rootXPath;
	private final ClassLoader classLoader;
	private final U rootNode;
	private final Class<U> nodeClass;
	private final Map<String, String> namespaces;

	public <T> T rebind(final Class<T> clazz) {

		nonNullArgument(clazz, "clazz");

		return binder.bind(configuration, rootNode, clazz);
	}

	public <T> T detach(final T proxy) {

		@SuppressWarnings("unchecked")
		final Class<T> clazz = (Class<T>) proxy.getClass();

		try {

			return detach(proxy, clazz);

		} catch (final NoSuchMethodException e) {
			throw new RuntimeException(e);
		} catch (final IllegalAccessException e) {
			throw new RuntimeException(e);
		} catch (final InvocationTargetException e) {
			throw new RuntimeException(e.getTargetException());
		}
	}

	private <T> T detach(final T proxy, final Class<T> clazz)
			throws InvocationTargetException, IllegalAccessException,
			SecurityException, NoSuchMethodException {

		final Map<Method, Object> values = new HashMap<Method, Object>();

		parseGetMethods(new GetMethodHandler() {

			@Override
			public boolean handleGetMethod(@Nullable final Method isNullMethod,
					final Method getterMethod) {

				final String methodName = getterMethod.getName();

				if ("node".equals(methodName)) { // DETACH
					return true;
				}

				final XPath xpathAnnotation = getSelfOrInheritedAnnotation(
						getterMethod, XPath.class);

				final XPath fieldXPath;

				if (xpathAnnotation != null) {

					fieldXPath = xpathAnnotation;

				} else {

					fieldXPath = findFieldXPathFromSimilar(methodName);
				}

				final Object value;

				try {

					value = getValue(fieldXPath, isNullMethod, getterMethod);

				} catch (final RuntimeException e) {
					throw e;
				} catch (final Error e) {
					throw e;
				} catch (final Throwable e) {
					throw new RuntimeException(e);
				}

				values.put(getterMethod, value == null ? Null.class : value);

				return true;
			}

			@Nullable
			private Object getValue(final XPath fieldXPath,
					@Nullable final Method isNullMethod,
					final Method getterMethod) throws Throwable {

				if (isNullMethod != null) {

					final boolean isNull = (Boolean) invokeGetter(fieldXPath,
							isNullMethod);

					if (isNull) {

						return null;
					}
				}

				return invokeGetter(fieldXPath, getterMethod);
			}
		});

		values.put(Object.class.getMethod("toString"), proxy.toString());

		values.put(Object.class.getMethod("hashCode"), proxy.hashCode());

		final Object detached = Proxy.newProxyInstance(proxy.getClass()
				.getClassLoader(), new Class[] { this.clazz },
				new DetachInvocationHandler<T>(clazz, values));

		return clazz.cast(detached);
	}

	protected final U getRootNode() {

		return rootNode;
	}

	@Override
	public final Object invoke(final Object proxy, final Method method,
			@Nullable final Object[] args) throws Throwable {

		final String methodName = method.getName();

		final Class<?>[] paramTypes = method.getParameterTypes();

		Method customMethod = method;

		// 1. SANITY CHECKS

		final boolean hasNoParam = paramTypes == null || paramTypes.length == 0;

		// 1.1. TOSTRING

		final boolean isToStringMethod = hasNoParam
				&& methodName.equals("toString");
		if (isToStringMethod) {
			try {
				customMethod = clazz.getMethod("toString");
			} catch (final NoSuchMethodException e) {
				return invokeToString(proxy);
			}
			if (customMethod == null
					|| !customMethod.isAnnotationPresent(XPath.class)) {

				// We didn't add an @XPath annotation to "toString()":

				return invokeToString(proxy);
			}
		}

		// 1.2. EQUALS

		final boolean isEqualsMethod = methodName.equals("equals")
				&& (paramTypes != null && paramTypes.length == 1);
		if (isEqualsMethod) {
			return invokeEquals(proxy, args[0]);
		}

		// 1.3. HASHCODE

		final boolean isHashCodeMethod = hasNoParam
				&& methodName.equals("hashCode");
		if (isHashCodeMethod) {
			return invokeHashCode(proxy);
		}

		// 2. XPATH

		final XPath xpathAnnotation = getSelfOrInheritedAnnotation(
				customMethod, XPath.class);

		final XPath fieldXPath;

		if (xpathAnnotation != null) {

			fieldXPath = xpathAnnotation;

		} else {

			fieldXPath = findFieldXPathFromSimilar(methodName);
		}

		if (fieldXPath == null) {
			throw new NotImplementedException(
					"Method has no @XPath annotation: " + method);
		}

		// 3. SET

		final boolean isSetMethod = methodName.startsWith("set");

		if (isSetMethod) {

			return invokeSetter(fieldXPath, method, args);
		}

		// 4. GET

		return invokeGetter(fieldXPath, method, args);
	}

	@Nullable
	private static Method getCustomMethodToString(final Class<?> clazz) {

		nonNullArgument(clazz, "clazz");

		final Method customMethodToString;

		try {

			customMethodToString = clazz.getMethod("toString");

		} catch (final NoSuchMethodException e) {

			return null;
		}

		if (customMethodToString != null
				&& customMethodToString.isAnnotationPresent(XPath.class)) {

			return customMethodToString;
		}

		return null;
	}

	private Object invokeGetter(final XPath fieldXPath, final Method method,
			@Nullable final Object... args) throws Throwable {

		nonNullArgument(fieldXPath, "fieldXPath");
		nonNullArgument(method, "method");

		final String methodName = method.getName();

		final boolean isIsNullMethod = methodName.startsWith("isNull");
		final boolean isSizeOfMethod = methodName.startsWith("sizeOf");
		final boolean isHasMethod = methodName.startsWith("has");
		final boolean isIsMethod = !isIsNullMethod
				&& methodName.startsWith("is");
		final boolean isDoesMethod = methodName.startsWith("does");
		final boolean isToMethod = methodName.startsWith("to");
		final boolean isAddToMethod = methodName.startsWith("addTo");
		final boolean isGetMethod = isHasMethod || isIsMethod || isDoesMethod
				|| isToMethod || methodName.startsWith("get");

		final Class<?> returnType = method.getReturnType();

		if (isIsNullMethod && !boolean.class.equals(returnType)
				&& !Boolean.class.equals(returnType)) {
			throw new NoSuchMethodException(
					"hasXxx() methods should return boolean: " + methodName
							+ "(): " + returnType);
		}

		if (isSizeOfMethod && !int.class.equals(returnType)
				&& !Integer.class.equals(returnType)) {
			throw new NoSuchMethodException(
					"sizeOfXxx() methods should return int: " + methodName
							+ "(): " + returnType);
		}

		final BinderXPathVariableResolver resolver = new DefaultBinderXPathVariableResolver(
				rootNode, method, args);

		final U node = evaluateToNode(rootXPath, resolver, rootNode);

		if (node == null) {

			final String failFunction = fieldXPath.failFunction();

			if (!isBlank(failFunction)) {

				return handleXPathFunction(failFunction, resolver, returnType);

			} else if (isIsNullMethod) {

				return true;

			} else if (isSizeOfMethod) {

				return 0;
			}

			throw newNullNodeException(rootXPath);
		}

		final Class<?> evalType;

		final boolean isArray = returnType.isArray();
		final boolean isList = List.class.isAssignableFrom(returnType);
		final boolean isMap = Map.class.isAssignableFrom(returnType);

		// 4. FIND WHICH TYPE WILL RETURN EVAL()

		if (isGetMethod) {

			if (isArray || isList || isMap) {

				evalType = NodeList.class;

			} else {

				evalType = returnType;
			}

		} else if (isIsNullMethod) {

			evalType = NodeList.class;

		} else if (isSizeOfMethod) {

			evalType = NodeList.class;

		} else if (isAddToMethod) {

			evalType = NodeList.class; // get the list nodeClass; // get the
										// parent

		} else {

			throw new NotImplementedException("Method: " + method);
		}

		// 5. GET THE ACTUAL VALUE

		final boolean isNullable = isGetMethod
				&& !evalType.isPrimitive() // && String.class.equals(evalType)
				&& (getSelfOrInheritedAnnotation(method, Nullable.class) != null //
				|| getSelfOrInheritedAnnotation(method,
						javax.annotation.Nullable.class) != null);

		final Object value = getValue(fieldXPath.value(),
				fieldXPath.function(), fieldXPath.failFunction(), resolver,
				node, evalType, isNullable, !isToMethod);

		if (isAddToMethod) {

			if (value == null) {

				throw new NotImplementedException("addTo a null array: "
						+ method);
			}

			final List<U> nodeList = asNodeList(value);

			final U parent;

			final String childName = calcChildName(method, fieldXPath);

			if (nodeList.isEmpty()) {

				parent = node;

			} else {

				parent = getParent(nodeList.get(0));
			}

			final U childNode = addToChildren(parent, childName);

			final Class<?> childType = method.getReturnType();

			final Object eval = binder.bind(
					BindConfiguration.newBuilder(configuration)
							.setXPathExpression(".").build(), childNode,
					classLoader, childType);

			return eval;

		} else if (isGetMethod) {

			if (isArray) {

				final List<U> nodeList = asNodeList(value);

				final Class<?> componentType = returnType.getComponentType();

				final int length = nodeList.size();

				final Object array = Array.newInstance(componentType, length);

				parseNodeList(nodeList, fieldXPath.function(), resolver,
						componentType, new CollectionBinder() {

							@Override
							public void bind(final int index, final Object value) {

								Array.set(array, index, value);
							}
						});

				return array;

			} else if (isList) {

				final List<Object> list = new ArrayList<Object>();

				final List<U> nodeList = asNodeList(value);

				final Class<?> componentType = fieldXPath
						.collectionComponentType();

				parseNodeList(nodeList, fieldXPath.function(), resolver,
						componentType, new CollectionBinder() {

							@Override
							public void bind(final int index, final Object value) {

								list.add(value);
							}
						});

				return list;

			} else if (isMap) {

				final Map<Object, Object> map = new HashMap<Object, Object>();

				final List<U> nodeList = asNodeList(value);

				parseNodeMap(nodeList, fieldXPath.mapKeysXPath(),
						fieldXPath.mapKeysFunction(), fieldXPath.mapKeysType(),
						fieldXPath.mapValuesXPath(),
						fieldXPath.mapValuesFunction(),
						fieldXPath.mapValuesType(), resolver, new MapBinder() {

							@Override
							public void bind(final Object key,
									final Object value) {

								map.put(key, value);
							}
						}, isToMethod);

				return map;

			} else {

				return value;
			}

		} else if (isIsNullMethod) {

			return asNodeList(value).size() == 0;

		} else if (isSizeOfMethod) {

			return (value == null) ? 0 : asNodeList(value).size();

		} else {

			return value;
		}
	}

	private static final Map<String, Annotation> selfOrInheritedAnnotations = new HashMap<String, Annotation>();

	private static final Set<String> nullSelfOrInheritedAnnotations = new HashSet<String>();

	// TODO move this method to avc-commons-lang?
	@Nullable
	private static <V extends Annotation> V getSelfOrInheritedAnnotation(
			final Method method, final Class<V> annotationClass) {

		nonNullArgument(method, "method");
		nonNullArgument(annotationClass, "annotationClass");

		final String cacheKey = method + "/@" + annotationClass.getName();

		final Annotation cached = selfOrInheritedAnnotations.get(cacheKey);

		if (cached != null) {
			return annotationClass.cast(cached);
		}

		if (nullSelfOrInheritedAnnotations.contains(cacheKey)) {
			return null;
		}

		final V annotation = method.getAnnotation(annotationClass);

		if (annotation != null) {

			selfOrInheritedAnnotations.put(cacheKey, annotation);

			return annotation;
		}

		final String methodName = method.getName();

		final Class<?> returnType = method.getReturnType();

		final Class<?>[] paramTypes = method.getParameterTypes();

		final Class<?> declaringClass = method.getDeclaringClass();

		for (final Class<?> i : declaringClass.getInterfaces()) {

			for (final Method m : i.getDeclaredMethods()) {

				if (methodName.equals(m.getName())
						&& m.getReturnType().isAssignableFrom(returnType)
						&& Arrays.deepEquals(m.getParameterTypes(), paramTypes)) {

					final V a = getSelfOrInheritedAnnotation(m, annotationClass);

					if (a != null) {

						selfOrInheritedAnnotations.put(cacheKey, a);

						return a;
					}
				}
			}
		}

		nullSelfOrInheritedAnnotations.add(cacheKey);

		return null;
	}

	private Boolean invokeEquals(final Object proxy, final Object arg)
			throws IllegalArgumentException, IllegalAccessException,
			InvocationTargetException {

		if (arg == null || !clazz.isAssignableFrom(arg.getClass())) {

			return FALSE;
		}

		if (proxy == arg) {

			return TRUE;
		}

		final Method customMethodToString = getCustomMethodToString(clazz);

		if (customMethodToString != null) {

			// TODO: Document this: hashCode() -> toString().equals(...)
			// IF TOSTRING() IS PRESENT, USE EQUALS(TOSTRING(), TOSTRING())

			return proxy.toString().equals(arg.toString());
		}

		final MutableBoolean result = new MutableBoolean(true);

		parseGetMethods(new GetMethodHandler() {

			@Override
			public boolean handleGetMethod(@Nullable final Method isNullMethod,
					final Method getterMethod) throws IllegalArgumentException,
					IllegalAccessException, InvocationTargetException {

				if (isNullMethod != null) {

					final boolean n0 = (Boolean) isNullMethod.invoke(proxy);

					final boolean n1 = (Boolean) isNullMethod.invoke(arg);

					if (n0 && n1) {

						return true;
					}
				}

				final Object v0 = getterMethod.invoke(proxy);

				final Object v1 = getterMethod.invoke(arg);

				if (v0 == null && v1 == null) {

					return true;
				}

				if (v0 == null || v1 == null || !v0.equals(v1)) {

					result.setValue(false);

					return false;
				}

				return true;
			}
		});

		return result.toBoolean();
	}

	private Integer invokeHashCode(final Object proxy)
			throws IllegalArgumentException, IllegalAccessException,
			InvocationTargetException {

		final Method customMethodToString = getCustomMethodToString(clazz);

		if (customMethodToString != null) {

			// TODO: Document this: hashCode() -> toString().hashCode()
			// IF TOSTRING() IS PRESENT, USE TOSTRING().HASHCODE()

			return proxy.toString().hashCode();
		}

		final MutableInt result = new MutableInt();

		parseGetMethods(new GetMethodHandler() {

			@Override
			public boolean handleGetMethod(@Nullable final Method isNullMethod,
					final Method getterMethod) throws IllegalArgumentException,
					IllegalAccessException, InvocationTargetException {

				if (isNullMethod != null) {

					final boolean isNull = (Boolean) isNullMethod.invoke(proxy);

					if (isNull) {

						return true;
					}
				}

				final Object value = getterMethod.invoke(proxy);

				if (value == null) {

					return true;
				}

				result.add(value.hashCode());

				return true;
			}
		});

		return result.toInteger();
	}

	private interface GetMethodHandler {

		/**
		 * @return <tt>true</tt> if we want to continue the loop.
		 */
		boolean handleGetMethod(@Nullable Method isNullMethod,
				Method getterMethod) throws IllegalArgumentException,
				IllegalAccessException, InvocationTargetException;
	}

	private void parseGetMethods(final GetMethodHandler handler)
			throws IllegalArgumentException, IllegalAccessException,
			InvocationTargetException {

		for (final Method getterMethod : clazz.getMethods()) {

			final int modifiers = getterMethod.getModifiers();

			if (Modifier.isStatic(modifiers) || !Modifier.isPublic(modifiers)) {

				continue;
			}

			final Class<?> returnType = getterMethod.getReturnType();
			final Class<?>[] paramTypes = getterMethod.getParameterTypes();

			if (returnType == null || returnType.equals(void.class)
					|| returnType.equals(Void.class)
					|| (paramTypes != null && paramTypes.length != 0)
					|| getterMethod.isSynthetic()) {

				continue;
			}

			final String methodName = getterMethod.getName();

			if ("toString".equals(methodName) || "hashCode".equals(methodName)) {

				continue;
			}

			Method isNullMethod = null;

			if (methodName.startsWith("get")) {

				try {

					isNullMethod = clazz.getMethod("isNull"
							+ substringAfter(methodName, "get"));

				} catch (final Exception e) {

					// do nothing
				}
			}
			final boolean breakLoop = !handler.handleGetMethod(isNullMethod,
					getterMethod);

			if (breakLoop) {

				break;
			}
		}
	}

	private String invokeToString(final Object proxy)
			throws IllegalArgumentException, IllegalAccessException,
			InvocationTargetException {

		throw new NotImplementedException(
				"Better annotate your toString() method with @XPath");
	}

	private static RuntimeException newNullNodeException(
			@Nullable final String xpathExpression) {

		return new IllegalArgumentException(
				"XPath evaluates to a null node: "
						+ xpathExpression
						+ ". Might consider to use a failFunction for your @XPath-annotation.");
	}

	private Object getValue(final String expression,
			@Nullable final String xpathFunction,
			@Nullable final String xpathFailFunction,
			final BinderXPathVariableResolver resolver, final U thisNode,
			final Class<?> evalType, final boolean isNullable,
			final boolean transtype) {

		nonNullArgument(expression, "expression");
		nonNullArgument(resolver, "resolver");
		nonNullArgument(thisNode, "thisNode");
		nonNullArgument(evalType, "evalType");

		resolver.setThisNode(thisNode);

		if (NodeList.class.equals(evalType)) {

			final List<U> eval = evaluateToList(expression, resolver, thisNode);

			return eval;

		} else if (int.class.equals(evalType)) {

			final double eval;

			if (isBlank(xpathFunction)) {

				eval = evaluateToNumber(expression, resolver, thisNode);

			} else {

				final U node = evaluateToNode(expression, resolver, thisNode);

				eval = handleXPathDoubleFunction(node, xpathFunction,
						xpathFailFunction, resolver);
			}

			return (int) eval;

		} else if (Integer.class.equals(evalType)) {

			final double eval;

			if (isBlank(xpathFunction) && !isNullable) {

				eval = evaluateToNumber(expression, resolver, thisNode);

			} else {

				final U node = evaluateToNode(expression, resolver, thisNode);

				if (node == null && isNullable) {

					return null;

				} else {

					eval = handleXPathDoubleFunction(node, xpathFunction,
							xpathFailFunction, resolver);
				}
			}

			return (int) eval;

		} else if (long.class.equals(evalType)) {

			final double eval;

			if (isBlank(xpathFunction)) {

				eval = evaluateToNumber(expression, resolver, thisNode);

			} else {

				final U node = evaluateToNode(expression, resolver, thisNode);

				eval = handleXPathDoubleFunction(node, xpathFunction,
						xpathFailFunction, resolver);
			}

			return (long) eval;

		} else if (float.class.equals(evalType)) {

			final double eval;

			if (isBlank(xpathFunction)) {

				eval = evaluateToNumber(expression, resolver, thisNode);

			} else {

				final U node = evaluateToNode(expression, resolver, thisNode);

				eval = handleXPathDoubleFunction(node, xpathFunction,
						xpathFailFunction, resolver);
			}

			return (float) eval;

		} else if (double.class.equals(evalType)) {

			final double eval;

			if (isBlank(xpathFunction)) {

				eval = evaluateToNumber(expression, resolver, thisNode);

			} else {

				final U node = evaluateToNode(expression, resolver, thisNode);

				eval = handleXPathDoubleFunction(node, xpathFunction,
						xpathFailFunction, resolver);
			}

			return (double) eval;

		} else if (Double.class.equals(evalType)) {

			final Double eval;

			if (isBlank(xpathFunction) && !isNullable) {

				eval = evaluateToNumber(expression, resolver, thisNode);

			} else {

				final U node = evaluateToNode(expression, resolver, thisNode);

				if (node == null && isNullable) {

					return null;

				} else {

					eval = handleXPathDoubleFunction(node, xpathFunction,
							xpathFailFunction, resolver);
				}
			}

			return (Double) eval;

		} else if (String.class.equals(evalType)) {

			final String eval;

			if (isBlank(xpathFunction) && !isNullable) {

				eval = evaluateToString(expression, resolver, thisNode);

			} else {

				final U node = evaluateToNode(expression, resolver, thisNode);

				if (node == null && isNullable) {

					return null;

				} else {

					eval = handleXPathStringFunction(node, xpathFunction,
							xpathFailFunction, resolver);
				}
			}

			return eval;

		} else if (char.class.equals(evalType)) {

			final String eval = evaluateToString(expression, resolver, thisNode);

			if (eval == null || eval.isEmpty()) {

				throw new NullPointerException("Cannot find XPath expression: "
						+ expression);
			}

			return eval.charAt(0);

		} else if (DateTime.class.equals(evalType)) {

			final String eval = evaluateToString(expression, resolver, thisNode);

			return TypeUtils.unmarshallValue(evalType, eval);

		} else if (boolean.class.equals(evalType)) {

			final boolean eval;

			if (isBlank(xpathFunction)) {

				eval = evaluateToBoolean(expression, resolver, thisNode);

			} else {

				final U node = evaluateToNode(expression, resolver, thisNode);

				eval = handleXPathBooleanFunction(node, xpathFunction,
						xpathFailFunction, resolver);
			}

			return eval;

		} else if (evalType.isEnum()) {

			final String eval = evaluateToString(expression, resolver, thisNode);

			return findEnumConstant(evalType, eval);

		} else if (!transtype && evalType.isAssignableFrom(rootNode.getClass())) {

			final U node = evaluateToNode(expression, resolver, thisNode);

			return node;

		} else {

			final U node = evaluateToNode(expression, resolver, thisNode);

			if (node == null) {

				if (isNullable) {

					return null;
				}

				throw new NullPointerException(
						"XPath expression evaluated to null: " + expression);
			}

			final Object eval = binder.bind(
					BindConfiguration.newBuilder(configuration)
							.setXPathExpression(".").build(), node,
					classLoader, evalType);

			return eval;
		}
	}

	private void parseNodeList(final List<U> nodeList,
			final String xpathFunction,
			final BinderXPathVariableResolver resolver,
			final Class<?> componentType,
			final CollectionBinder collectionBinder) {

		nonNullArgument(nodeList, "nodeList");
		nonNullArgument(componentType, "componentType");
		nonNullArgument(collectionBinder, "collectionBinder");

		final int length = nodeList.size();

		for (int i = 0; i < length; ++i) {

			final U node = nodeList.get(i);

			final Object elementValue;

			if (String.class.equals(componentType)) {

				elementValue = handleXPathStringFunction(node, xpathFunction,
						resolver);

			} else {

				elementValue = binder.bind(
						BindConfiguration.newBuilder(configuration)
								.setXPathExpression(".").build(), node,
						classLoader, componentType);
			}

			collectionBinder.bind(i, elementValue);
		}
	}

	private void parseNodeMap(final List<U> nodeList, final String keysXPath,
			final String keysXPathFunction, final Class<?> keysType,
			final String valuesXPath, final String valuesXPathFunction,
			final Class<?> valuesType,
			final BinderXPathVariableResolver resolver,
			final MapBinder mapBinder, final boolean transtype) {

		nonNullArgument(nodeList, "nodeList");
		nonNullArgument(keysXPath, "keysXPath");
		nonNullArgument(keysXPathFunction, "keysXPathFunction");
		nonNullArgument(keysType, "keysType");
		nonNullArgument(valuesXPath, "valuesXPath");
		nonNullArgument(valuesXPathFunction, "valuesXPathFunction");
		nonNullArgument(valuesType, "valuesType");
		nonNullArgument(resolver, "resolver");
		nonNullArgument(mapBinder, "mapBinder");

		final int length = nodeList.size();

		final boolean NO_NULLABLE = false;
		final boolean TRANSTYPE = true;

		for (int i = 0; i < length; ++i) {

			final U node = nodeList.get(i);

			final Object key = evaluateToObject(keysXPath, keysXPathFunction,
					resolver, node, keysType, NO_NULLABLE, TRANSTYPE);

			final Object value = evaluateToObject(valuesXPath,
					valuesXPathFunction, resolver, node, valuesType,
					NO_NULLABLE, transtype);

			mapBinder.bind(key, value);
		}
	}

	private Object evaluateToObject(final String expression,
			final String xpathFunction,
			final BinderXPathVariableResolver resolver, final U node,
			final Class<?> type, final boolean isNullable,
			final boolean transtype) {

		final boolean isArray = type.isArray();
		final boolean isList = List.class.isAssignableFrom(type);
		final boolean isMap = Map.class.isAssignableFrom(type);

		if (isArray || isList || isMap) {

			throw new NotImplementedException("Type: " + type);
		}

		return getValue(expression, xpathFunction, null, resolver, node, type,
				isNullable, transtype);
	}

	@SuppressWarnings("unchecked")
	private List<U> asNodeList(final Object value) {

		nonNullArgument(value, "value");

		return (List<U>) value;
	}

	private static final Map<String, XPath> fieldXPathFromSimilars = new HashMap<String, XPath>();

	private static final Set<String> nullFieldXPathFromSimilars = new HashSet<String>();

	/**
	 * find the XPath expression for a given field (in fact for a given method
	 * name) among other methods corresponding to the same field.
	 */
	protected final XPath findFieldXPathFromSimilar(final String methodName) {

		final String cacheKey = clazz.getName() + "#" + methodName;

		final XPath cached = fieldXPathFromSimilars.get(cacheKey);

		if (cached != null) {
			return cached;
		}

		if (nullFieldXPathFromSimilars.contains(cacheKey)) {
			return null;
		}

		final String methodSuffix;

		if (methodName.startsWith("sizeOf") || methodName.startsWith("isNull")) {

			methodSuffix = methodName.substring(6);

		} else if (methodName.startsWith("get") || methodName.startsWith("has")) {

			methodSuffix = methodName.substring(3);

		} else if (methodName.startsWith("is")) {

			methodSuffix = methodName.substring(2);

		} else if (methodName.startsWith("does")) {

			methodSuffix = methodName.substring(4);

		} else if (methodName.startsWith("set")) {

			methodSuffix = methodName.substring(3);

		} else if (methodName.startsWith("addTo")) {

			methodSuffix = methodName.substring(5);

		} else {

			throw new NotImplementedException("Method name: " + methodName);
		}

		for (final Method method : clazz.getMethods()) {

			final XPath xpathAnnotation = method.getAnnotation(XPath.class);

			if (xpathAnnotation != null) {

				final String annotatedMethodName = method.getName();

				if (annotatedMethodName.equals("get" + methodSuffix)
						|| annotatedMethodName.equals("has" + methodSuffix)
						|| annotatedMethodName.equals("is" + methodSuffix)
						|| annotatedMethodName.equals("does" + methodSuffix)
						|| annotatedMethodName.equals("set" + methodSuffix)
						|| annotatedMethodName.equals("isNull" + methodSuffix)
						|| annotatedMethodName.equals("sizeOf" + methodSuffix)) {

					fieldXPathFromSimilars.put(cacheKey, xpathAnnotation);

					return xpathAnnotation;
				}
			}
		}

		nullFieldXPathFromSimilars.add(cacheKey);

		return null;
	}

	/**
	 * evaluate a XPath expression. Similar to "<tt>newXPath().evaluate()</tt>"
	 * but with more logs in case of error.
	 */
	protected abstract U evaluateToNode(String expression,
			BinderXPathVariableResolver resolver, @Nullable U node)
			throws BindingException;

	/**
	 * evaluate a XPath expression. Similar to "<tt>newXPath().evaluate()</tt>"
	 * but with more logs in case of error.
	 */
	protected abstract double evaluateToNumber(String expression,
			BinderXPathVariableResolver resolver, @Nullable U node)
			throws BindingException;

	/**
	 * evaluate a XPath expression. Similar to "<tt>newXPath().evaluate()</tt>"
	 * but with more logs in case of error.
	 */
	protected abstract String evaluateToString(String expression,
			BinderXPathVariableResolver resolver, @Nullable U node)
			throws BindingException;

	/**
	 * evaluate a XPath expression. Similar to "<tt>newXPath().evaluate()</tt>"
	 * but with more logs in case of error.
	 */
	protected abstract boolean evaluateToBoolean(String expression,
			BinderXPathVariableResolver resolver, @Nullable U node)
			throws BindingException;

	/**
	 * evaluate a XPath expression. Similar to "<tt>newXPath().evaluate()</tt>"
	 * but with more logs in case of error.
	 */
	protected abstract List<U> evaluateToList(String expression,
			BinderXPathVariableResolver resolver, @Nullable U node)
			throws BindingException;

	/**
	 * return the text content of a given node.
	 */
	protected abstract String getTextContent(@Nullable U node);

	/**
	 * apply an XPath function, if necessary, to a node found, or return its
	 * text content.
	 */
	private String handleXPathStringFunction(@Nullable final U node,
			final String xpathFunction,
			final BinderXPathVariableResolver resolver) {

		if (isBlank(xpathFunction)) {

			return getTextContent(node);

		} else {

			return evaluateToString(xpathFunction, resolver, node);
		}
	}

	/**
	 * apply an XPath function, if necessary, to a node found, or return its
	 * text content.
	 */
	private String handleXPathStringFunction(@Nullable final U node,
			final String xpathFunction, final String xpathFailFunction,
			final BinderXPathVariableResolver resolver) {

		if (node == null) {

			// do nothing
		}

		if (isBlank(xpathFunction)) {

			return getTextContent(node);

		} else {

			return evaluateToString(xpathFunction, resolver, node);
		}
	}

	/**
	 * apply an XPath function with no context.
	 */
	private Object handleXPathFunction(@Nullable final String xpathFunction,
			final BinderXPathVariableResolver resolver, final Class<?> evalType) {

		final U node = null;

		if (String.class.equals(evalType)) {

			throw new NotImplementedException("Type: " + evalType);

		} else if (int.class.equals(evalType)) {

			final double value = handleXPathDoubleFunction(node, xpathFunction,
					resolver);

			return (int) value;

		} else if (boolean.class.equals(evalType)) {

			return handleXPathBooleanFunction(node, xpathFunction, resolver);

		} else {

			throw new NotImplementedException("Type: " + evalType);
		}
	}

	/**
	 * apply an XPath function, if necessary, to a node found, or return its
	 * text content.
	 */
	private double handleXPathDoubleFunction(@Nullable final U node,
			final String xpathFunction, final String xpathFailFunction,
			final BinderXPathVariableResolver resolver) {

		if (node != null) {

			return handleXPathDoubleFunction(node, xpathFunction, resolver);

		} else if (!isBlank(xpathFailFunction)) {

			return handleXPathDoubleFunction(node, xpathFailFunction, resolver);

		} else {

			throw newNullNodeException(xpathFunction);
		}
	}

	/**
	 * apply an XPath function, if necessary, to a node found, or return its
	 * text content.
	 */
	private double handleXPathDoubleFunction(@Nullable final U node,
			final String xpathFunction, BinderXPathVariableResolver resolver) {

		if (isBlank(xpathFunction)) {

			return Double.parseDouble(getTextContent(node));

		} else {

			return evaluateToNumber(xpathFunction, resolver, node);
		}
	}

	/**
	 * apply an XPath function, if necessary, to a node found, or return its
	 * text content.
	 */
	private boolean handleXPathBooleanFunction(final U node,
			final String xpathFunction,
			final BinderXPathVariableResolver resolver) {

		if (isBlank(xpathFunction)) {

			return Boolean.parseBoolean(getTextContent(node));

		} else {

			return evaluateToBoolean(xpathFunction, resolver, node);
		}
	}

	/**
	 * apply an XPath function, if necessary, to a node found, or return its
	 * text content.
	 */
	private boolean handleXPathBooleanFunction(@Nullable final U node,
			final String xpathFunction, final String xpathFailFunction,
			final BinderXPathVariableResolver resolver) {

		if (node != null) {

			return handleXPathBooleanFunction(node, xpathFunction, resolver);

		} else if (!isBlank(xpathFailFunction)) {

			return handleXPathBooleanFunction(node, xpathFailFunction, resolver);

		} else {

			throw newNullNodeException(xpathFunction);
		}
	}

	protected final BindConfiguration getConfiguration() {

		return configuration;
	}

	private final BindConfiguration configuration;

	protected final String getNamespaceURI(final String prefix) {

		nonNullArgument(prefix, "prefix");

		final String nsURI = namespaces.get(prefix);

		if (nsURI == null) {
			throw new RuntimeException("Unknown namespaceURI for prefix="
					+ prefix + ":");
		}

		return nsURI;
	}

	protected static Map<String, String> calcNamespaceMap(final Class<?> clazz) {

		nonNullArgument(clazz, "class");

		final Map<String, String> uris = new HashMap<String, String>();

		addNamespaceMap(uris, clazz);

		return uris;
	}

	private static void addNamespaceMap(final Map<String, String> uris,
			@Nullable final Class<?> clazz) {

		nonNullArgument(uris, "uris");

		if (clazz == null) {
			return;
		}

		final Namespaces nsAnnotation = clazz.getAnnotation(Namespaces.class);

		if (nsAnnotation != null) {

			final String[] ns = nsAnnotation.value();

			final Map<String, String> map = XPathUtils.calcNamespaceMap(ns);

			for (final Map.Entry<String, String> entry : map.entrySet()) {

				final String key = entry.getKey();

				if (!uris.containsKey(key)) { // Subclass overrides

					uris.put(key, entry.getValue());
				}
			}
		}

		for (final Class<?> i : clazz.getInterfaces()) {

			addNamespaceMap(uris, i);
		}

		// addNamespaceMap(uris, clazz.getSuperclass());

		addNamespaceMap(uris, clazz.getEnclosingClass());
	}

	protected static Collection<BindFunctions> calcFunctions(
			final Class<?> clazz) {

		nonNullArgument(clazz, "class");

		final Collection<BindFunctions> functions = new ArrayList<BindFunctions>();

		addFunctionsCollection(functions, clazz);

		return functions;
	}

	private static void addFunctionsCollection(
			final Collection<BindFunctions> functions,
			@Nullable final Class<?> clazz) {

		nonNullArgument(functions, "functions");

		if (clazz == null) {
			return;
		}

		final Functions fnsAnnotation = clazz.getAnnotation(Functions.class);

		if (fnsAnnotation != null) {

			final Class<? extends BindFunctions>[] fnsClasses = fnsAnnotation
					.value();

			for (final Class<? extends BindFunctions> fnsClass : fnsClasses) {

				final BindFunctions fns;

				final Constructor<? extends BindFunctions> defaultConstructor;

				try {

					defaultConstructor = fnsClass.getConstructor();

				} catch (final NoSuchMethodException e) {
					throw new RuntimeException(
							"Default constructor not found for BindFunctions class: "
									+ fnsClass.getName(), e);
				}

				defaultConstructor.setAccessible(true);

				try {

					fns = defaultConstructor.newInstance();

				} catch (final IllegalAccessException e) {
					throw new RuntimeException(
							"Cannot instantiate BindFunctions class: "
									+ fnsClass.getName(), e);
				} catch (final InstantiationException e) {
					throw new RuntimeException(
							"Cannot instantiate BindFunctions class: "
									+ fnsClass.getName(), e);
				} catch (final InvocationTargetException e) {
					throw new RuntimeException(
							"Cannot instantiate BindFunctions class: "
									+ fnsClass.getName(), e);
				}

				functions.add(fns);
			}
		}

		for (final Class<?> i : clazz.getInterfaces()) {

			addFunctionsCollection(functions, i);
		}

		// addNamespaceMap(uris, clazz.getSuperclass());

		addFunctionsCollection(functions, clazz.getEnclosingClass());
	}

	private Object invokeSetter(final XPath fieldXPath, final Method method,
			@Nullable final Object[] args) throws Throwable {

		nonNullArgument(fieldXPath, "fieldXPath");
		nonNullArgument(method, "method");

		final Class<?> returnType = method.getReturnType();

		final boolean returnThisInstance = returnType.isAssignableFrom(clazz);

		if (returnType != null //
				&& !void.class.equals(returnType)
				&& !Void.class.equals(returnType) //
				&& !returnThisInstance) {
			throw new IllegalStateException(
					"Illegal return type for setter method: "
							+ returnType.getName() + " (" + method.getName()
							+ ")");
		}

		final BinderXPathVariableResolver resolver = new DefaultBinderXPathVariableResolver(
				rootNode, method, args);

		final U node = evaluateToNode(rootXPath, resolver, rootNode);

		final boolean NO_NULLABLE = false;
		final boolean NO_TRANSTYPE = false;

		final String xpathExpression = fieldXPath.value();

		final Object v = getValue(xpathExpression, fieldXPath.function(),
				fieldXPath.failFunction(), resolver, node, nodeClass,
				NO_NULLABLE, NO_TRANSTYPE);

		final U n = nodeClass.cast(v);

		final Object value;

		if (args == null || args.length == 0) {

			value = null;

		} else {

			value = args[args.length - 1];
		}

		if (n == null) {

			// a new node must be created

			createValue(rootNode, xpathExpression, value);

		} else {

			// node exists

			updateValue(n, xpathExpression, value);
		}

		return returnThisInstance ? getInstance() : null;
	}

	@Nullable
	private U getChild(final U node, final String childName) {

		nonNullArgument(node, "node");
		nonNullArgument(childName, "childName");

		for (final U child : getChildren(node)) {

			if (childName.equals(getName(child))) {

				return child;
			}
		}

		return null;
	}

	private void createValue(final U node, final String expression,
			final Object value) {

		nonNullArgument(node, "node");
		nonNullArgument(expression, "expression");
		nonNullArgument(value, "value");

		// TODO All this is not very accurate

		if (expression.startsWith("@")) {

			final String attributeName = expression.substring(1);

			setAttribute(node, attributeName, value);

			return;
		}

		if (expression.contains("/@")) {

			final String elementName = substringBefore(expression, "/@");

			final String attributeName = substringAfter(expression, "/@");

			final U hic = evaluateToNode(elementName, null, node);

			final U child;

			if (hic != null) {

				child = hic;

			} else if (elementName.contains("[@")
					&& !elementName.contains("]/")) {

				final String selector = substringBetween(expression, "[@", "]");

				final String selectorName = substringBefore(selector, "=")
						.trim();

				final String selectorValue = substringBetween(
						substringAfter(selector, "="), "'");

				child = addToChildren(node, substringBefore(elementName, "[")
						.trim());

				setAttribute(child, selectorName, selectorValue);

			} else {

				child = recursiveCreateChildren(node, elementName);
			}

			setAttribute(child, attributeName, value);

			return;
		}

		final U child = recursiveCreateChildren(node, expression);

		setValue(child, value);
	}

	private U recursiveCreateChildren(final U node, final String expression) {

		final U hic = evaluateToNode(expression, null, node);

		if (hic != null) {
			return hic;
		}

		final String before;
		final String after;

		U parent;

		if (!expression.contains("/")) {

			before = null;
			after = expression;

			parent = node;

		} else {

			before = substringBeforeLast(expression, "/");
			after = substringAfterLast(expression, "/");

			parent = evaluateToNode(before, null, node);
		}

		if (parent == null) {

			parent = recursiveCreateChildren(node, before);
		}

		return addToChildren(parent, after);

	}

	private void updateValue(final U node, final String expression,
			final Object value) {

		nonNullArgument(node, "node");
		nonNullArgument(expression, "expression");
		nonNullArgument(value, "value");

		if (value instanceof Node) {

			setNode(node, value);

			return;
		}

		// TODO not very accurate

		if (expression.contains("@")) {

			setAttribute(node, value);

			return;
		}

		if (expression.contains("/@")) {

			final String elementName = substringBefore(expression, "/@");
			final String attributeName = substringAfter(expression, "/@");

			if (elementName.contains("/")) {
				throw new NotImplementedException("expression: " + expression);
			}

			final U child = evaluateToNode(elementName, null, node);

			setAttribute(child, attributeName, value);

			return;
		}

		if (!expression.contains("/")) {

			setValue(node, value);

			return;
		}

		setValue(node, value);
	}

	@SuppressWarnings("unchecked")
	static <U> InstanceUpdater<U> getAbstractBinderInvocationHandler(
			final Object proxy, final U node) {

		nonNullArgument(proxy, "proxy");
		nonNullArgument(node, "node");

		final InvocationHandler invocationHandler = Proxy
				.getInvocationHandler(proxy);

		if (AbstractBinderInvocationHandler.class.isInstance(invocationHandler)) {

			return (AbstractBinderInvocationHandler<U>) invocationHandler;
		}

		return new DummyInstanceUpdater<U>(proxy);
	}

	public XPathFunctionResolver createDomFunctionResolver(
			@Nullable final U node, final Iterable<BindFunctions> functions) {

		nonNullArgument(functions, "functions");

		return new XPathFunctionResolver() {

			@Override
			public String toString() {

				return functions.toString();
			}

			@Override
			public XPathFunction resolveDomFunction(
					@Nullable final String namespaceURI, final String localPart) {
				// final int arity) {

				nonNullArgument(localPart, "localPart");

				// Jaxen passes functions with no namespace, such as "count()"!
				// Jaxp doesn't.
				// Let's do the minimum.
				if (namespaceURI == null) {
					return null;
				}

				final List<XPathFunction> domFunctions = new ArrayList<XPathFunction>();

				for (final BindFunctions fn : functions) {

					if (fn.matchesNamespaceURI(namespaceURI)) {

						for (final Method method : fn.getClass().getMethods()) {

							final String[] xpathFunctionNames = getXPathFunctionNames(method);

							if (!ArrayUtils.contains(xpathFunctionNames,
									localPart)) {
								continue;
							}

							final Class<?>[] paramTypes = method
									.getParameterTypes();

							if (paramTypes == null || paramTypes.length == 0) {
								continue;
							}

							// First parameter must be Node.
							if (!paramTypes[0].isAssignableFrom(nodeClass)) {
								continue;
							}

							domFunctions.add(new XPathFunctionImpl<U>(node,
									localPart, method, fn));
						}
					}
				}

				if (domFunctions.isEmpty()) {

					return null; // XPath function has not been found

				} else if (domFunctions.size() == 1) {

					return domFunctions.iterator().next();

				} else {

					return new XPathFunctionComposite(domFunctions);
				}
			}
		};
	}

	/**
	 * return the XPath function's name declared in some annotation, or the Java
	 * method's name itself if no such annotation is present.
	 */
	// TODO cache these values
	private static String[] getXPathFunctionNames(final Method method) {

		nonNullArgument(method, "method");

		final List<String> names = new ArrayList<String>();

		names.add(method.getName());

		final XPathFunctionNames xpathFunctionNamesAnnotation = method
				.getAnnotation(XPathFunctionNames.class);

		if (xpathFunctionNamesAnnotation != null) {

			names.addAll(Arrays.asList(xpathFunctionNamesAnnotation.value()));
		}

		// TODO do the same for overriden *and* overloaded methods

		return Iterables.toArray(names, String.class);
	}

	/**
	 * return the tag name that will be used for a child from the given method.
	 * The resolution is at follows:
	 * <ol>
	 * <li>If the return type of the "<tt>addToXxx()</tt>" method, that is the
	 * child type, has a {@link XPath} annotation itself, use the element name
	 * declared in its XPath expression.
	 * <li>Otherwise, use the XPath expression declared on the "
	 * <tt>addToXxx()</tt>" method.
	 * <li>Why, yes, we could also the Java's simple name of the return type of
	 * the "<tt>addToXxx()</tt>" method, that is the child type.
	 * <ul>
	 * 
	 * @param method
	 *            the "<tt>addToXxx()</tt>" method.
	 * @param fieldXPath
	 *            the XPath annotation attached to the method (or one of its
	 *            similar siblings, such as "<tt>getXxx()</tt>", "<tt>
	 *            sizeOfXxx()"<tt>, etc.)
	 */
	private static String calcChildName(final Method method,
			final XPath fieldXPath) {

		nonNullArgument(method, "method");
		nonNullArgument(fieldXPath, "fieldXPath");

		final Class<?> childType = method.getReturnType();

		final XPath xpathAnnotation = childType.getAnnotation(XPath.class);

		if (xpathAnnotation != null) {

			return calcChildName(xpathAnnotation);
		}

		// if (isValidChildName(fieldXPath))
		return calcChildName(fieldXPath);

		// if (true)
		// throw new NotImplementedException(
		// "Child type has no XPath annotation: "
		// + childType.getName());
		//
		// return childType.getSimpleName();
	}

	private static String calcChildName(final XPath xpath) {

		nonNullArgument(xpath, "xpath");

		final String expression = xpath.value();

		if (expression.contains("/") || expression.contains("[")
				|| expression.contains("|") || expression.contains("@")) {
			throw new NotImplementedException("addTo() on XPath expression: "
					+ expression);
		}

		return expression;
	}
}
